/* description.cc
   Mathieu Stefani, 24 février 2016

   Implementation of the description system
*/

#include <pistache/config.h>
#include <pistache/description.h>
#include <pistache/http_header.h>

#include <algorithm>
#include <sstream>

namespace Pistache {
namespace Rest {

const char *schemeString(Scheme scheme) {
  switch (scheme) {
#define SCHEME(e, str)                                                         \
  case Scheme::e:                                                              \
    return str;
    SCHEMES
#undef SCHEME
  }

  return nullptr;
}

namespace Schema {

Contact::Contact(std::string name, std::string url, std::string email)
    : name(std::move(name)), url(std::move(url)), email(std::move(email)) {}

License::License(std::string name, std::string url)
    : name(std::move(name)), url(std::move(url)) {}

Info::Info(std::string title, std::string version, std::string description)
    : title(std::move(title)), version(std::move(version)),
      description(std::move(description)), termsOfService(), contact(),
      license() {}

PathDecl::PathDecl(std::string value, Http::Method method)
    : value(std::move(value)), method(method) {}

Path::Path(std::string value, Http::Method method, std::string description)
    : value(std::move(value)), method(method),
      description(std::move(description)), hidden(false), pc(), parameters(),
      responses(), handler() {}

std::string Path::swaggerFormat(const std::string &path) {
  if (path.empty())
    return "";
  if (path[0] != '/')
    throw std::invalid_argument("Invalid path, should start with a '/'");

  /* @CodeDup: re-use the private Fragment class of Router */
  auto isPositional = [](const std::string &fragment) {
    if (fragment[0] == ':')
      return std::make_pair(true, fragment.substr(1));
    return std::make_pair(false, std::string());
  };

  auto isOptional = [](const std::string &fragment) {
    auto pos = fragment.find('?');
    // @Validation the '?' should be the last character
    return std::make_pair(pos != std::string::npos, pos);
  };

  std::ostringstream oss;

  auto processFragment = [&](std::string fragment) {
    auto optional = isOptional(fragment);
    if (optional.first)
      fragment.erase(optional.second, 1);

    auto positional = isPositional(fragment);
    if (positional.first) {
      oss << '{' << positional.second << '}';
    } else {
      oss << fragment;
    }
  };

  std::istringstream iss(path);
  std::string fragment;

  std::vector<std::string> fragments;

  while (std::getline(iss, fragment, '/')) {
    fragments.push_back(std::move(fragment));
  }

  for (size_t i = 0; i < fragments.size() - 1; ++i) {
    const auto &frag = fragments[i];

    processFragment(frag);
    oss << '/';
  }

  const auto &last = fragments.back();
  if (last.empty())
    oss << '/';
  else {
    processFragment(last);
  }

  return oss.str();
}

bool PathGroup::hasPath(const std::string &name, Http::Method method) const {
  auto group = paths(name);
  auto it = std::find_if(std::begin(group), std::end(group),
                         [&](const Path &p) { return p.method == method; });
  return it != std::end(group);
}

bool PathGroup::hasPath(const Path &path) const {
  return hasPath(path.value, path.method);
}

PathGroup::Group PathGroup::paths(const std::string &name) const {
  auto it = groups_.find(name);
  if (it == std::end(groups_))
    return PathGroup::Group{};

  return it->second;
}

Optional<Path> PathGroup::path(const std::string &name,
                               Http::Method method) const {
  auto group = paths(name);
  auto it = std::find_if(std::begin(group), std::end(group),
                         [&](const Path &p) { return p.method == method; });

  if (it != std::end(group)) {
    return Optional<Path>(Some(*it));
  }
  return Optional<Path>(None());
}

PathGroup::group_iterator PathGroup::add(Path path) {
  if (hasPath(path))
    return PathGroup::group_iterator{};

  auto &group = groups_[path.value];
  return group.insert(group.end(), std::move(path));
}

PathGroup::const_iterator PathGroup::begin() const { return groups_.begin(); }

PathGroup::const_iterator PathGroup::end() const { return groups_.end(); }

PathGroup::flat_iterator PathGroup::flatBegin() const {
  return makeFlatMapIterator(groups_, begin());
}

PathGroup::flat_iterator PathGroup::flatEnd() const {
  return makeFlatMapIterator(groups_, end());
}

PathBuilder::PathBuilder(Path *path) : path_(path) {}

SubPath::SubPath(std::string prefix, PathGroup *paths)
    : prefix(std::move(prefix)), parameters(), paths(paths) {}

PathBuilder SubPath::route(const std::string &name, Http::Method method,
                           std::string description) {
  auto fullPath = prefix + name;
  Path path(std::move(fullPath), method, std::move(description));
  std::copy(std::begin(parameters), std::end(parameters),
            std::back_inserter(path.parameters));

  auto it = paths->add(std::move(path));

  return PathBuilder(&*it);
}

PathBuilder SubPath::route(PathDecl fragment, std::string description) {
  return route(std::move(fragment.value), fragment.method,
               std::move(description));
}

SubPath SubPath::path(const std::string &prefix) {
  return SubPath(this->prefix + prefix, paths);
}

Parameter::Parameter(std::string name, std::string description)
    : name(std::move(name)), description(std::move(description)),
      required(true), type() {}

Response::Response(Http::Code statusCode, std::string description)
    : statusCode(statusCode), description(std::move(description)) {}

ResponseBuilder::ResponseBuilder(Http::Code statusCode, std::string description)
    : response_(statusCode, std::move(description)) {}

InfoBuilder::InfoBuilder(Info *info) : info_(info) {}

InfoBuilder &InfoBuilder::termsOfService(std::string value) {
  info_->termsOfService = std::move(value);
  return *this;
}

InfoBuilder &InfoBuilder::contact(std::string name, std::string url,
                                  std::string email) {
  info_->contact =
      Some(Contact(std::move(name), std::move(url), std::move(email)));
  return *this;
}

InfoBuilder &InfoBuilder::license(std::string name, std::string url) {
  info_->license = Some(License(std::move(name), std::move(url)));
  return *this;
}

} // namespace Schema

Description::Description(std::string title, std::string version,
                         std::string description)
    : info_(std::move(title), std::move(version), std::move(description)),
      host_(), basePath_(), schemes_(), pc(), paths_() {}

Schema::InfoBuilder Description::info() {
  Schema::InfoBuilder builder(&info_);
  return builder;
}

Description &Description::host(std::string value) {
  host_ = std::move(value);
  return *this;
}

Description &Description::basePath(std::string value) {
  basePath_ = std::move(value);
  return *this;
}

Schema::PathDecl Description::options(std::string name) {
  return Schema::PathDecl(std::move(name), Http::Method::Options);
}

Schema::PathDecl Description::get(std::string name) {
  return Schema::PathDecl(std::move(name), Http::Method::Get);
}

Schema::PathDecl Description::post(std::string name) {
  return Schema::PathDecl(std::move(name), Http::Method::Post);
}

Schema::PathDecl Description::head(std::string name) {
  return Schema::PathDecl(std::move(name), Http::Method::Head);
}

Schema::PathDecl Description::put(std::string name) {
  return Schema::PathDecl(std::move(name), Http::Method::Put);
}

Schema::PathDecl Description::patch(std::string name) {
  return Schema::PathDecl(std::move(name), Http::Method::Patch);
}

Schema::PathDecl Description::del(std::string name) {
  return Schema::PathDecl(std::move(name), Http::Method::Delete);
}

Schema::PathDecl Description::trace(std::string name) {
  return Schema::PathDecl(std::move(name), Http::Method::Trace);
}

Schema::PathDecl Description::connect(std::string name) {
  return Schema::PathDecl(std::move(name), Http::Method::Connect);
}

Schema::SubPath Description::path(std::string name) {
  return Schema::SubPath(std::move(name), &paths_);
}

Schema::PathBuilder Description::route(std::string name, Http::Method method,
                                       std::string description) {
  auto it = paths_.emplace(std::move(name), method, std::move(description));
  return Schema::PathBuilder(&*it);
}

Schema::PathBuilder Description::route(Schema::PathDecl fragment,
                                       std::string description) {
  return route(std::move(fragment.value), fragment.method,
               std::move(description));
}

Schema::ResponseBuilder Description::response(Http::Code statusCode,
                                              std::string description) {
  Schema::ResponseBuilder builder(statusCode, std::move(description));
  return builder;
}

Swagger &Swagger::uiPath(std::string path) {
  uiPath_ = std::move(path);
  return *this;
}

Swagger &Swagger::uiDirectory(std::string dir) {
  uiDirectory_ = std::move(dir);
  return *this;
}

Swagger &Swagger::apiPath(std::string path) {
  apiPath_ = std::move(path);
  return *this;
}

Swagger &Swagger::serializer(Swagger::Serializer serialize) {
  serializer_ = std::move(serialize);
  return *this;
}

void Swagger::install(Rest::Router &router) {

  Route::Handler uiHandler = [=](const Rest::Request &req,
                                 Http::ResponseWriter response) {
    auto res = req.resource();

    /*
     * @Export might be useful for routing also. Make it public or merge it with
     * the Fragment class
     */

    struct Path {
      explicit Path(const std::string &value)
          : value(value), trailingSlashValue(value) {
        if (trailingSlashValue.empty() || trailingSlashValue.back() != '/')
          trailingSlashValue += '/';
      }

      static bool hasTrailingSlash(const Rest::Request &req) {
        auto res_ = req.resource();
        return res_.back() == '/';
      }

      std::string stripPrefix(const Rest::Request &req) {
        auto res_ = req.resource();
        if (!res_.compare(0, value.size(), value)) {
          return res_.substr(value.size());
        }
        return res_;
      }

      bool matches(const Rest::Request &req) const {
        auto res_ = req.resource();
        if (value == res_)
          return true;

        if (res_ == trailingSlashValue)
          return true;

        return false;
      }

      bool isPrefix(const Rest::Request &req) {
        auto res_ = req.resource();
        return !res_.compare(0, value.size(), value);
      }

      const std::string &withTrailingSlash() const {
        return trailingSlashValue;
      }

      std::string join(const std::string &value) const {
        std::string val;
        if (value[0] == '/')
          val = value.substr(1);
        else
          val = value;
        return trailingSlashValue + val;
      }

    private:
      std::string value;
      std::string trailingSlashValue;
    };

    Path ui(uiPath_);
    Path uiDir(uiDirectory_);

    if (ui.matches(req)) {
      if (!Path::hasTrailingSlash(req)) {
        response.headers().add<Http::Header::Location>(uiPath_ + '/');

        response.send(Http::Code::Moved_Permanently);
      } else {
        auto index = uiDir.join("index.html");
        Http::serveFile(response, index);
      }
      return Route::Result::Ok;
    } else if (ui.isPrefix(req)) {
      auto file = ui.stripPrefix(req);
      auto path = uiDir.join(file);
      Http::serveFile(response, path);
      return Route::Result::Ok;
    }

    else if (res == apiPath_) {
      response.send(Http::Code::Ok, serializer_(description_),
                    MIME(Application, Json));
      return Route::Result::Ok;
    }

    return Route::Result::Failure;
  };

  router.addCustomHandler(uiHandler);
}

} // namespace Rest
} // namespace Pistache
